$persistentStore
$persistentStore 为抓包规则脚本提供一个小型键值存储,跨运行持久化。可用它在多次请求之间记住一个 token、一个计数器,或任意小段字符串。存在你自己指定的键下的值,所有抓包脚本都能看到;不带键存的那一个只属于你自己的脚本。它仅在抓包规则脚本中可用。
方法
read返回存储的字符串,键不存在时返回null。write存入一个字符串并返回true;值的类型不受支持或超过大小上限时返回false且什么都不存(见「说明」)。值传null表示删除该键,与remove等价。compareAndWrite只在该键此刻正好是expected时才写,并返回是否写成了。expected传null表示「这个键还不该存在」。比较通过之后它的行为与write完全一致 —— 所以值传null就是一次条件删除。remove删除该键并返回true。
省略 key 时使用一个默认键。这个默认键属于脚本本身,而不属于装它进来的那个模块:跑同一个脚本的两条规则共用它 —— 包括分处两个不同模块、但指向同一个 script-path 的规则 —— 不同的脚本各有各的。想在不同脚本之间共享同一个值,请显式传 key。
只有一种例外:复制出来的模块副本,即使指向同一个脚本,也从一个属于它自己的空默认键开始。正因如此,同一个模块可以复制两份同时用(比如一份配一个账号)而互不覆盖。副本内部的多条规则之间照常共用。
读改写不会被打断
同一段同步代码里的 read → 比较 → write 不会被别的脚本插进来。所有抓包脚本跑在同一个 JavaScript 环境、同一条线程上,而这条线程不会在一段同步代码中间切去跑另一个脚本。所以即使同时有很多请求在处理,下面这段也是安全的:
这个保证在第一个异步操作处结束。 $httpClient 的回调、Promise、定时器,每一个都会让别的脚本在这个间隙里跑起来,于是之前读到的值,等回调跑起来时可能已经过期:
如果只是要一个计数器,在回调里面重新读一次、把比较和写放在那里的同一段同步代码中就够了。
提交一个在异步之前读到的值
当读到的那个值正是这次请求要用的入参时,「在回调里重读一遍」是没用的 —— 比如拿 revision 5 去服务端换一个新 token:等结果回来时你不能直接写下去,因为别人可能已经推进到了 revision 6,而你拿到的 token 是给一个不再是当前值的 revision 用的。
compareAndWrite 把这个前提一直带到写的那一刻:
expected 传 null 表示「这个键还不该存在」,可以让恰好一个脚本完成创建:
重试必须有次数上限。 compareAndWrite 返回 false 有两种原因,只有一种值得重试:
- 别人先写成功了。脚本日志里不会有任何记录 —— 这是并发下的正常结果。
- 值或键被拒了(对象、
undefined、超过大小上限、保留前缀的键)。脚本日志里会有一条提示,而且再重试多少次都是同样的结果。
所有脚本共用一条线程,所以一个永远成功不了的 while (!$persistentStore.compareAndWrite(...)) 卡住的不只是你自己的脚本,而是所有脚本。请数着次数写:
这个保证到哪里为止。 它在抓包脚本之间成立 —— 它们共用同一个 JavaScript 环境。它不覆盖这个环境之外发生的改动:在 app 自己的界面里改一个值、或者脚本引擎被系统回收后重建,都会把脚本看到的那份换成磁盘上的那份。
说明
- 值为字符串。数字和布尔会被转换;对象和数组会被拒绝(
write返回false)—— 请先用JSON.stringify序列化,读出来时用JSON.parse解析。 write(undefined, key)同样会被拒绝,原有的值原封不动。这是有意的:undefined通常来自一个不存在的头或字段,因为它把一个好好的值删掉,多半不是你的本意。真要删请传null。- 单个值最大 4 MB。超过时
write返回false且不存任何东西,脚本日志里会有一条说明。 - 值会一直保留到被删除。重装 app 会清空。
- 键不能以
scope:开头。这个前缀留给「不带键时每个脚本各自的那一格」,用它作键就能直接写进别的脚本的槽位。read、write、remove遇到这样的键会拒绝执行,脚本日志里会有一条说明。 - 值以明文保存。可以放 session token 这类东西,但不要放你不希望别的脚本看到的内容 —— 任何脚本只要知道键名就能读到。
